/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
package software.amazon.smithy.traitcodegen.test;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.example.traits.StringTrait;
import com.example.traits.defaults.StructDefaultsTrait;
import com.example.traits.documents.DocumentTrait;
import com.example.traits.documents.StructWithNestedDocumentTrait;
import com.example.traits.enums.EnumListMemberTrait;
import com.example.traits.enums.IntEnumTrait;
import com.example.traits.enums.MyEnumTrait;
import com.example.traits.enums.MyIntEnumTrait;
import com.example.traits.enums.StringEnumTrait;
import com.example.traits.enums.SuitTrait;
import com.example.traits.lists.DocumentListTrait;
import com.example.traits.lists.ListMember;
import com.example.traits.lists.MyUnion;
import com.example.traits.lists.NestedListTrait;
import com.example.traits.lists.NestedUniqueItemsListTrait;
import com.example.traits.lists.NumberListTrait;
import com.example.traits.lists.StringListTrait;
import com.example.traits.lists.StructureListTrait;
import com.example.traits.lists.UnionListTrait;
import com.example.traits.maps.MapValue;
import com.example.traits.maps.NestedMapTrait;
import com.example.traits.maps.NestedStringUniqueItemMapTrait;
import com.example.traits.maps.StringDocumentMapTrait;
import com.example.traits.maps.StringStringMapTrait;
import com.example.traits.maps.StringToStructMapTrait;
import com.example.traits.maps.StringToUnionMapTrait;
import com.example.traits.mixins.StructWithMixinTrait;
import com.example.traits.mixins.StructureListWithMixinMemberTrait;
import com.example.traits.mixins.UnionWithMixinMemberTrait;
import com.example.traits.names.SnakeCaseStructureTrait;
import com.example.traits.numbers.BigDecimalTrait;
import com.example.traits.numbers.BigIntegerTrait;
import com.example.traits.numbers.ByteTrait;
import com.example.traits.numbers.DoubleTrait;
import com.example.traits.numbers.FloatTrait;
import com.example.traits.numbers.IntegerTrait;
import com.example.traits.numbers.LongTrait;
import com.example.traits.numbers.ShortTrait;
import com.example.traits.structures.BasicAnnotationTrait;
import com.example.traits.structures.EnumA;
import com.example.traits.structures.EnumB;
import com.example.traits.structures.NestedA;
import com.example.traits.structures.NestedB;
import com.example.traits.structures.StructMemberWithTimestampFormatTrait;
import com.example.traits.structures.StructWithEnumDefaultTrait;
import com.example.traits.structures.StructWithIdrefMemberTrait;
import com.example.traits.structures.StructWithListOfMapTrait;
import com.example.traits.structures.StructWithUnionTrait;
import com.example.traits.structures.StructWithUniqueItemsListTrait;
import com.example.traits.structures.StructureTrait;
import com.example.traits.timestamps.DateTimeTimestampTrait;
import com.example.traits.timestamps.EpochSecondsTimestampTrait;
import com.example.traits.timestamps.HttpDateTimestampTrait;
import com.example.traits.timestamps.TimestampTrait;
import com.example.traits.unions.NestedUnionA;
import com.example.traits.unions.Type;
import com.example.traits.unions.UnionTrait;
import com.example.traits.uniqueitems.NumberSetTrait;
import com.example.traits.uniqueitems.SetMember;
import com.example.traits.uniqueitems.StringSetTrait;
import com.example.traits.uniqueitems.StructureSetTrait;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.smithy.model.SourceLocation;
import software.amazon.smithy.model.node.ArrayNode;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.NumberNode;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.TraitFactory;
import software.amazon.smithy.utils.ListUtils;
import software.amazon.smithy.utils.MapUtils;
import software.amazon.smithy.utils.SetUtils;

public class CreatesTraitTest {
    private static final ShapeId DUMMY_ID = ShapeId.from("ns.foo#foo");
    private final TraitFactory provider = TraitFactory.createServiceFactory();
    private static final SourceLocation testLocation = new SourceLocation("test.smithy", 1, 2);

    static Stream<Arguments> createTraitTests() {
        return Stream.of(
                // Document traits
                Arguments.of(DocumentTrait.ID,
                        Node.objectNodeBuilder()
                                .withMember("metadata", "woo")
                                .withMember("more", "yay")
                                .build()),
                Arguments.of(StructWithNestedDocumentTrait.ID,
                        ObjectNode.objectNodeBuilder()
                                .withMember("doc",
                                        ObjectNode.builder()
                                                .withMember("foo", "bar")
                                                .withMember("fizz", "buzz")
                                                .build())
                                .build()),
                // Enums
                Arguments.of(StringEnumTrait.ID, Node.from("no")),
                Arguments.of(IntEnumTrait.ID, Node.from(2)),
                Arguments.of(SuitTrait.ID, Node.from("clubs")),
                Arguments.of(EnumListMemberTrait.ID,
                        ObjectNode.objectNodeBuilder()
                                .withMember("value", ArrayNode.fromStrings("some", "none", "some"))
                                .build()),
                Arguments.of(MyIntEnumTrait.ID, Node.from(1)),
                Arguments.of(MyEnumTrait.ID, Node.from("1")),
                // Lists
                Arguments.of(NumberListTrait.ID,
                        ArrayNode.fromNodes(
                                Node.from(1),
                                Node.from(2),
                                Node.from(3))),
                Arguments.of(StringListTrait.ID, ArrayNode.fromStrings("a", "b", "c")),
                Arguments.of(StructureListTrait.ID,
                        ArrayNode.fromNodes(
                                ListMember.builder().a("first").b(1).c("other").build().toNode(),
                                ListMember.builder().a("second").b(2).c("more").build().toNode())),
                Arguments.of(DocumentListTrait.ID,
                        ArrayNode.fromNodes(
                                ObjectNode.builder().withMember("a", "b").build(),
                                ObjectNode.builder().withMember("c", "d").withMember("e", "f").build())),
                Arguments.of(NestedListTrait.ID,
                        ArrayNode.fromNodes(
                                ArrayNode.fromNodes(
                                        ArrayNode.fromNodes(Node.from("1"), Node.from("2"), Node.from("3"))))),
                Arguments.of(NestedUniqueItemsListTrait.ID,
                        ArrayNode.fromNodes(
                                ArrayNode.fromNodes(
                                        ArrayNode.fromNodes(Node.from("1"), Node.from("2"), Node.from("3"))))),
                Arguments.of(UnionListTrait.ID,
                        ArrayNode.fromNodes(
                                MyUnion.builder().unitVariantMember().build().toNode(),
                                MyUnion.builder().integerVariantMember(1).build().toNode())),
                // Maps
                Arguments.of(StringStringMapTrait.ID,
                        StringStringMapTrait.builder()
                                .putValues("a", "first")
                                .putValues("b", "other")
                                .build()
                                .toNode()),
                Arguments.of(StringToStructMapTrait.ID,
                        StringToStructMapTrait.builder()
                                .putValues("one", MapValue.builder().a("foo").b(2).build())
                                .putValues("two", MapValue.builder().a("bar").b(4).build())
                                .build()
                                .toNode()),
                Arguments.of(StringDocumentMapTrait.ID,
                        StringDocumentMapTrait.builder()
                                .putValues("a", ObjectNode.builder().withMember("a", "a").build().toNode())
                                .putValues("b", ObjectNode.builder().withMember("b", "b").build().toNode())
                                .putValues("string", Node.from("stuff"))
                                .putValues("number", Node.from(1))
                                .build()
                                .toNode()),
                Arguments.of(NestedMapTrait.ID,
                        NestedMapTrait.builder()
                                .putValues("1", MapUtils.of("1", MapUtils.of("2", "3")))
                                .build()
                                .toNode()),
                Arguments.of(NestedStringUniqueItemMapTrait.ID,
                        NestedStringUniqueItemMapTrait.builder()
                                .putValues("1",
                                        SetUtils.of(
                                                MapUtils.of("2", "3"),
                                                MapUtils.of("4", "5")))
                                .build()
                                .toNode()),
                Arguments.of(StringToUnionMapTrait.ID,
                        StringToUnionMapTrait.builder()
                                .putValues("1",
                                        com.example.traits.maps.MyUnion.builder()
                                                .integerVariantMember(1)
                                                .build())
                                .build()
                                .toNode()),
                // Mixins
                Arguments.of(StructureListWithMixinMemberTrait.ID,
                        ArrayNode.fromNodes(ObjectNode.builder().withMember("a", "a").withMember("d", "d").build())),
                Arguments.of(StructWithMixinTrait.ID,
                        StructWithMixinTrait.builder()
                                .d("d")
                                .build()
                                .toNode()),
                Arguments.of(UnionWithMixinMemberTrait.ID,
                        UnionWithMixinMemberTrait.builder()
                                .unitVariantMember()
                                .build()
                                .toNode()),
                // Naming Conflicts
                Arguments.of(SnakeCaseStructureTrait.ID,
                        ObjectNode.builder()
                                .withMember("snake_case_member", "stuff")
                                .build()),
                // Numbers
                Arguments.of(BigDecimalTrait.ID, Node.from(1)),
                Arguments.of(BigIntegerTrait.ID, Node.from(1)),
                Arguments.of(ByteTrait.ID, Node.from(1)),
                Arguments.of(DoubleTrait.ID, Node.from(1.2)),
                Arguments.of(FloatTrait.ID, Node.from(1.2)),
                Arguments.of(IntegerTrait.ID, Node.from(1)),
                Arguments.of(LongTrait.ID, Node.from(1L)),
                Arguments.of(ShortTrait.ID, Node.from(1)),
                // Structures
                Arguments.of(BasicAnnotationTrait.ID, Node.objectNode()),
                Arguments.of(StructureTrait.ID,
                        StructureTrait.builder()
                                .fieldA("a")
                                .fieldB(true)
                                .fieldC(NestedA.builder()
                                        .fieldN("nested")
                                        .fieldQ(false)
                                        .fieldZ(NestedB.B)
                                        .build())
                                .fieldD(ListUtils.of("a", "b", "c"))
                                .fieldE(MapUtils.of("a", "one", "b", "two"))
                                .build()
                                .toNode()),
                Arguments.of(StructWithListOfMapTrait.ID,
                        StructWithListOfMapTrait.builder()
                                .addItems(MapUtils.of("1", "2"))
                                .addItems(MapUtils.of("3", "4"))
                                .build()
                                .toNode()),
                Arguments.of(StructWithUniqueItemsListTrait.ID,
                        StructWithUniqueItemsListTrait.builder()
                                .addItems(SetUtils.of("a", "b", "c"))
                                .build()
                                .toNode()),
                Arguments.of(StructWithIdrefMemberTrait.ID,
                        StructWithIdrefMemberTrait.builder()
                                .idRefMemberA(ShapeId.from("test.smithy.traitcodegen#a"))
                                .idRefMemberB(ShapeId.from("test.smithy.traitcodegen#b"))
                                .build()
                                .toNode()),
                Arguments.of(StructMemberWithTimestampFormatTrait.ID,
                        StructMemberWithTimestampFormatTrait.builder()
                                .memberDateTime(Instant.parse("1985-04-12T23:20:50.52Z"))
                                .memberHttpDate(Instant.from(
                                        DateTimeFormatter.RFC_1123_DATE_TIME.parse("Tue, 29 Apr 2014 18:30:38 GMT")))
                                .memberEpochSeconds(Instant.ofEpochSecond((long) 1515531081.123))
                                .build()
                                .toNode()),
                Arguments.of(StructWithEnumDefaultTrait.ID,
                        StructWithEnumDefaultTrait.builder()
                                .memberA(EnumA.TWO)
                                .memberB(EnumB.FOUR)
                                .build()
                                .toNode()),
                Arguments.of(StructWithUnionTrait.ID,
                        StructWithUnionTrait.builder()
                                .myUnion(com.example.traits.structures.MyUnion.builder().unitVariantMember().build())
                                .build()
                                .toNode()),
                // Timestamps
                Arguments.of(TimestampTrait.ID, Node.from("1985-04-12T23:20:50.52Z")),
                Arguments.of(DateTimeTimestampTrait.ID, Node.from("1985-04-12T23:20:50.52Z")),
                Arguments.of(HttpDateTimestampTrait.ID, Node.from("Tue, 29 Apr 2014 18:30:38 GMT")),
                Arguments.of(EpochSecondsTimestampTrait.ID, Node.from(1515531081.123)),
                // Unique Items (sets)
                Arguments.of(NumberSetTrait.ID,
                        ArrayNode.fromNodes(
                                Node.from(1),
                                Node.from(2),
                                Node.from(3))),
                Arguments.of(StringSetTrait.ID, ArrayNode.fromStrings("a", "b", "c")),
                Arguments.of(StructureSetTrait.ID,
                        ArrayNode.fromNodes(
                                SetMember.builder().a("first").b(1).c("other").build().toNode(),
                                SetMember.builder().a("second").b(2).c("more").build().toNode())),
                // Unions
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .unitVariantMember()
                                .build()
                                .toNode()),
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .stringVariantMember(ShapeId.from("test.abc#myShape"))
                                .build()
                                .toNode()),
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .integerVariantMember(123)
                                .build()
                                .toNode()),
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .structVariantMember(
                                        Type.builder()
                                                .memberA("123")
                                                .build())
                                .build()
                                .toNode()),
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .unionVariantMember(
                                        NestedUnionA.builder()
                                                .unitVariantMember()
                                                .build())
                                .build()
                                .toNode()),
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .listVariantMember(ListUtils.of("1", "2", "3"))
                                .build()
                                .toNode()),
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .setVariantMember(SetUtils.of("1", "2", "3"))
                                .build()
                                .toNode()),
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .mapVariantMember(MapUtils.of("a", "b"))
                                .build()
                                .toNode()),
                Arguments.of(UnionTrait.ID,
                        UnionTrait.builder()
                                .timestampVariantMember(
                                        Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME
                                                .parse("Tue, 29 Apr 2014 18:30:38 GMT")))
                                .build()
                                .toNode()),
                // Strings
                Arguments.of(StringTrait.ID, Node.from("SPORKZ SPOONS YAY! Utensils.")),
                // Defaults
                Arguments.of(StructDefaultsTrait.ID, Node.objectNode()));
    }

    @ParameterizedTest
    @MethodSource("createTraitTests")
    void createsTraitFromNode(ShapeId traitId, Node fromNode) {
        Trait trait = provider.createTrait(traitId, DUMMY_ID, fromNode).orElseThrow(RuntimeException::new);
        assertEquals(SourceLocation.NONE, trait.getSourceLocation());
        assertEquals(trait, provider.createTrait(traitId, DUMMY_ID, trait.toNode()).orElseThrow(RuntimeException::new));
    }

    static Stream<Arguments> createSourceLocationTests() {
        return Stream.of(
                Arguments.of(BigDecimalTrait.ID, new NumberNode(1, testLocation)),
                Arguments.of(BigIntegerTrait.ID, new NumberNode(1, testLocation)),
                Arguments.of(ByteTrait.ID, new NumberNode(1, testLocation)),
                Arguments.of(DoubleTrait.ID, new NumberNode(1.2, testLocation)),
                Arguments.of(FloatTrait.ID, new NumberNode(1.2, testLocation)),
                Arguments.of(IntegerTrait.ID, new NumberNode(1, testLocation)),
                Arguments.of(LongTrait.ID, new NumberNode(1L, testLocation)),
                Arguments.of(ShortTrait.ID, new NumberNode(1, testLocation)),
                Arguments.of(StringTrait.ID, new StringNode("a", testLocation)),
                Arguments.of(TimestampTrait.ID, new StringNode("1985-04-12T23:20:50.52Z", testLocation)),
                Arguments.of(NumberListTrait.ID,
                        ArrayNode.builder()
                                .withValue(Node.from(1))
                                .withValue(Node.from(2))
                                .withValue(Node.from(3))
                                .sourceLocation(testLocation)
                                .build()
                                .toNode()),
                Arguments.of(NumberSetTrait.ID,
                        ArrayNode.builder()
                                .withValue(Node.from(1))
                                .withValue(Node.from(2))
                                .withValue(Node.from(3))
                                .sourceLocation(testLocation)
                                .build()
                                .toNode()),
                Arguments.of(NestedMapTrait.ID,
                        NestedMapTrait.builder()
                                .putValues("1", MapUtils.of("1", MapUtils.of("2", "3")))
                                .sourceLocation(testLocation)
                                .build()
                                .toNode()),
                Arguments.of(StructWithListOfMapTrait.ID,
                        StructWithListOfMapTrait.builder()
                                .addItems(MapUtils.of("1", "2"))
                                .addItems(MapUtils.of("3", "4"))
                                .sourceLocation(testLocation)
                                .build()
                                .toNode()));
    }

    @ParameterizedTest
    @MethodSource("createSourceLocationTests")
    void sourceLocationTest(ShapeId traitId, Node fromNode) {
        Trait trait = provider.createTrait(traitId, DUMMY_ID, fromNode).orElseThrow(RuntimeException::new);
        assertEquals(testLocation, trait.getSourceLocation());
    }
}
